﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

using log4net;

namespace MND.Users
{
    class User
    {
        private static readonly ILog log = LogManager.GetLogger(typeof(User));

        #region Registration&Logon
        private static DBUsersDataContext usersDB = new DBUsersDataContext();

        private static List<string> bookedNames = new List<string>();

        private static bool _bookName(string userName)
        {
            lock (usersDB)
            {
                lock (bookedNames)
                {
                    if (usersDB.dbUsers.Any(user => user.Name.ToLower().Equals(userName.ToLower())))
                        return false;
                    if (bookedNames.Contains(userName))
                        return false;

                    bookedNames.Add(userName);
                    return true;
                }
            }
        }
        public bool BookName(string userName)
        {
            return _bookName(userName);
        }

        private static string _register(string userName, string userPass, string userEmail)
        {
            lock (usersDB)
            {
                lock (bookedNames)
                {
                    try
                    {
                        // 1. Prepare & verify username

                        userName = userName.Trim();



                        if (!userName.All(Char.IsLetterOrDigit))
                            throw new RegistrationException(Language.LanguageManager.Instance.GetString("REGISTRATION_USERNAME_NOT_VALID"));

                        if (usersDB.dbBannedUserNames.Any(bannedName => bannedName.BannedName.ToLower().Equals(userName.ToLower())))
                        {
                            throw new RegistrationException(Language.LanguageManager.Instance.GetString("REGISTRATION_USERNAME_BANNED"));
                        }


                        var dbusers = (from dbu in usersDB.dbUsers
                                       where dbu.Name.ToLower().Equals(userName.ToLower())
                                       select dbu);

                        if (dbusers.Count() > 0)
                            throw new RegistrationException(Language.LanguageManager.Instance.GetString("REGISTRATION_USER_ALREADY_EXISTS"));

                        // 2. if set, verify email
                        userEmail = userEmail.Trim();
                        if (userEmail.Length > 0)
                            if (!System.Text.RegularExpressions.Regex.IsMatch(userEmail,
                      @"^(?("")("".+?""@)|(([0-9a-zA-Z]((\.(?!\.))|[-!#\$%&'\*\+/=\?\^`\{\}\|~\w])*)(?<=[0-9a-zA-Z])@))" +
                      @"(?(\[)(\[(\d{1,3}\.){3}\d{1,3}\])|(([0-9a-zA-Z][-\w]*[0-9a-zA-Z]\.)+[a-zA-Z]{2,6}))$"))
                                throw new RegistrationException(Language.LanguageManager.Instance.GetString("REGISTRATION_EMAIL_INVALID"));

                        // 3. salt & calculate pass hash
                        Random rand = new Random();
                        byte[] randomBytes = new byte[36];
                        rand.NextBytes(randomBytes);
                        string salt = Convert.ToBase64String(randomBytes).Substring(0, 10);
                        System.Security.Cryptography.SHA512Managed sha = new System.Security.Cryptography.SHA512Managed();
                        byte[] passHash = sha.ComputeHash(new UTF8Encoding().GetBytes(salt + userPass));

                        // 4. create user
                        dbUser dbuser = new dbUser() { Name = userName, Password = passHash, PassSalt = salt, Email = userEmail };

                        // 5. commit
                        usersDB.dbUsers.InsertOnSubmit(dbuser);

                        usersDB.SubmitChanges();
                       
                        return "OK";
                    }
                    catch (RegistrationException re)
                    {
                        return re.Message;
                    }
                    finally
                    {
                        if (bookedNames.Contains(userName))
                            bookedNames.Remove(userName);
                    }
                }
            }
        }
        public string Register(string userName, string userPass, string userEmail)
        {
            return _register(userName, userPass, userEmail);
        }
        internal class RegistrationException : Exception
        {
            public RegistrationException(string message)
                : base(message)
            { }
        }

        private static bool _login(string userName, string userPass)
        {
            lock (usersDB)
            {
                try
                {
                    var user = (from u in usersDB.dbUsers
                               where u.Name.ToLower().Equals(userName.ToLower())
                               select u).First();


                    System.Security.Cryptography.SHA512Managed sha = new System.Security.Cryptography.SHA512Managed();
                    byte[] passHash = sha.ComputeHash(new UTF8Encoding().GetBytes(user.PassSalt + userPass));
                    string uHash = Convert.ToBase64String(passHash);
                    string cHash = Convert.ToBase64String(user.Password.ToArray());
                    if (cHash != uHash)
                    {
                        throw new System.Security.SecurityException("Invalid username or password");
                    }
                    else return true;
                }
                catch (Exception e)
                {
                    log.InfoFormat("Exception while logon ({0}:[{1} chars]).", userName, userPass.Length);
                    log.Info("Exception:", e);
                    return false;
                }
            }
        }
        public bool Login(string userName, string userPass)
        {
            if (_login(userName, userPass))
            {
                var user = (from u in usersDB.dbUsers
                            where u.Name.ToLower().Equals(userName.ToLower())
                            select u).First();

                id = user.ID;
                name = user.Name;

                currentScenario = null;
                isLoggedIn = true;

                return true;
            }
            else return false;
        }
        #endregion
        
        private long id;
        public long Id { get { return id; } }

        private string name;
        public string Name { get { return name; } }


        private UserConfig config;
        public UserConfig Config { get { return config; } }

        public event EventHandler<EventArgs> LoggedOut;

        private Telnet.IMessageSource connection;

        private Encoding UserEncoding { get; set; }


        private Characters.PChar currentChar;
        public Characters.PChar CurrentCharacter { get { return currentChar; } }
        
        private bool isLoggedIn = false;
        public bool IsLoggedIn { get { return isLoggedIn; } }

        private Scenario currentScenario;
        //private Queue<Commands.UserCommand> userCommandQueue = new Queue<Commands.UserCommand>();
        private UserManager parentManager;

        public User(Telnet.IMessageSource pConnection, UserManager parent)
        {
            connection = pConnection;
            parentManager = parent;
            UserEncoding = new ASCIIEncoding();
            connection.ConnectionClosed += new EventHandler<EventArgs>(connection_ConnectionClosed);
            currentScenario = new Login.LoginScenario(this);
        }

        void connection_ConnectionClosed(object sender, EventArgs e)
        {
            Logout();
        }

        public void Logout()
        {

            EventHandler<EventArgs> lOut = LoggedOut;
            if (lOut != null)
                lOut(this, EventArgs.Empty);
        }

        public void Send(string s)
        {
            lock (connection)
            {
                connection.Send(s, UserEncoding);
            }
        }

        public string Read()
        {
            string s = String.Empty;
            lock (connection)
            {
                s = connection.GetNextMessage();
            }
            if (s == null)
                return null;
            return s;
            //return InterceptMessage(s);
        }

        
        public void Tick()
        {
            string message = Read();
            if (message != null)
            {
                if (currentScenario != null)
                {
                    currentScenario.Enqueue(message);
                    currentScenario.Tick();
                }
                else
                {
                    try
                    {
                        Commands.UserCommand uc = new Commands.UserCommand(message, this);
                        Commands.CommandManager.Instance.SetCommandBase(uc);
                        if (uc.Base.Scope == Commands.CommandScope.Char)
                        {
                            if (CurrentCharacter == null)
                                throw new Commands.CommandRequiresCharacterException();
                            else
                                CurrentCharacter.Order(uc);
                        }
                        else
                        {
                            parentManager.EnqueueCommand(uc);
                        }
                    }
                    catch (Commands.CommandNotFoundException cnfe)
                    {
                        if (cnfe.FoundCommands != null)
                        {
                            // Some commands like input% exist:
                            StringBuilder sb = new StringBuilder();
                            sb.AppendLine(Language.LanguageManager.Instance.GetString("AVAIL_COMMANDS_HEADER"));
                            sb.Append(String.Join(Environment.NewLine, cnfe.FoundCommands));
                            Send(sb.ToString());
                        }
                        else
                        {
                            // No such command:
                            Send(Language.LanguageManager.Instance.GetString("NO_SUCH_COMMANDS"));
                        }
                    }
                    catch (Commands.CommandRequiresCharacterException crce)
                    {
                        // User tried to use in-game command outside
                        Send(Language.LanguageManager.Instance.GetString("COMMAND_REQUIRES_CHARACTER"));
                    }
                    catch (Exception e)
                    {
                        log.Error("Error occured while parsing command!", e);
                    }
                }
            }
        }

        private string InterceptMessage(string s)
        {
            if (currentScenario != null)
            {
                currentScenario.Enqueue(s);
                currentScenario.Tick();
                return null;
            }
            return s;
        }

    }
}
